Postgres 语言服务器:初始版本正式发布
文章背景与核心概要
经过长达两年的研发历程(涵盖深入调研、架构转变以及 Rust 语言的学习),Postgres 语言服务器(Postgres Language Server) 的首个版本现已正式发布。这是一个实现语言服务器协议(LSP)的工具集,旨在为开发者提供可靠的 SQL 编写体验。
本文详细介绍了该语言服务器的核心架构、技术挑战以及文档生命周期管理。通过结合 libpg_query 进行精确解析、利用 tree-sitter 处理不完整语句,并借助内存中的 SchemaCache 提供上下文感知的自动补全和类型检查,该项目成功克服了 SQL 语法复杂且缺乏直接文件依赖关系的难题。此外,文章还展望了其未来在 WebAssembly(Wasm)和 PL/pgSQL 支持方面的演进方向。
Postgres Language Server: Initial Release
After a two-year development journey involving research, architectural shifts, and learning Rust, the initial release of the Postgres Language Server is officially here. It is a Language Server Protocol (LSP) implementation and a collection of developer tools designed for reliable SQL authoring.
此版本提供以下核心功能:
* 自动补全
* 语法错误高亮
* 类型检查(通过 EXPLAIN 错误洞察实现)
* 代码检查(Linter),灵感来源于 Squawk
This release provides: * Autocompletion * Syntax Error Highlighting * Type-checking (via
EXPLAINerror insights) * Lining, inspired by Squawk
可用性与安装
您可以通过各种编辑器扩展和命令行(CLI)包来使用该语言服务器:
- 编辑器:
- VSCode 扩展
- Neovim(通过
nvim-lspconfig+mason) - CLI 二进制文件:
- GitHub Releases(预编译二进制文件)
- npm 包(
@postgrestools/postgrestools)
Availability and Installation
You can access the Language Server through various editor extensions and CLI packages:
- Editors:
- VSCode Extension
- Neovim (via
nvim-lspconfig+mason)- CLI Binaries:
- GitHub Releases (Precompiled binaries)
- npm (
@postgrestools/postgrestools)For further details, check out the documentation or the GitHub repository.
语言服务器的架构
选择正确的数据模型在很大程度上取决于目标语言。例如,C++ 由于其头文件和“先声明后使用”的规则,会编译一次头文件并将其缓存。TypeScript 则会编译源文件(如 foo.ts)来解析依赖文件的类型。
然而,对于 SQL 和 Postgres 而言,规则则有所不同: * 数据库架构(Schema)是任何类型信息的唯一真实数据源(Single Source of Truth)。 * 不存在直接的文件到文件(file-to-file)关系。 * 架构管理工具各不相同,这意味着我们无法预设源代码的结构。
因此,Postgres 语言服务器作出了如下假设: 1. 编译的最小单位是单个语句(Single Statement)。 2. 数据库架构是任何类型信息的唯一真实数据源。 3. 所有语句都是独立的:为了理解任何语句,我们只需要解析该特定语句,而不需要其他内容。
The Architecture of a Language Server
Choosing the right data model depends heavily on the target language. For example, C++ compiles headers once and caches them because of its header files and declaration-before-use rules. TypeScript compiles source files (like
foo.ts) to resolve types for dependent files.For SQL and Postgres, however, the rules are different: * The database schema is the single source of truth for any type information. * There is no direct file-to-file relation. * Schema management tools vary widely, meaning we cannot assume how source code is structured.
Therefore, the Postgres Language Server assumes that: 1. The smallest unit of compilation is a single statement. 2. The database schema is the single source of truth for any type information. 3. All statements are independent: to make sense of any statement, we only need to parse that specific statement and nothing else.
技术挑战
该项目最复杂的部分是解析器(Parser)。由于 Postgres 拥有不断演进且复杂的语法,从头开始编写自定义解析器极其困难。
为了绕过这一难题,该项目利用 libpg_query 来可靠地解析 SQL 代码。然而,由于 libpg_query 是为可执行 SQL 设计的,而非针对语言智能(Language Intelligence),开发团队必须围绕文档生命周期构建出务实的解决方案。
Technical Challenges
The most complex part of the project was the parser. Because Postgres has an ever-evolving and complex syntax, writing a custom parser from scratch is exceptionally difficult.
To bypass this, the project utilizes libpg_query to parse SQL code reliably. However, since
libpg_queryis designed for executable SQL rather than language intelligence, the development team had to build pragmatic solutions around the document lifecycle.
文档生命周期
1. 拆分源码
当打开新文档时,一个自定义的语句拆分器会将文件(其中可能包含无效或不完整的语句)切分为独立的语句,以便 libpg_query 能够独立处理它们。
受 普拉特解析法(Pratt Parsing) 启发,该拆分器使用了智能启发式算法(例如,深知除非作为子语句,否则 SELECT 后面不能紧跟另一个 SELECT)。如果解析失败,它会退回到按分号或双换行符进行拆分。
2. 识别语句
文档中的每个语句都会被分配一个唯一标识符,并通过 Statement 结构体进行追踪:
/// 全局唯一的语句
#[derive(Hash)]
pub(crate) struct Statement {
/// 文档路径
pub(crate) path: PgTPath,
/// 文档内的唯一 ID
pub(crate) id: StatementId,
}
3. 解析语句
服务器在这些语句上同时运行 tree-sitter 和 libpg_query:
* libpg_query 提供精确的解析、语法错误检测以及用于诊断的 AST(抽象语法树)结构。
* tree-sitter 即使面对格式错误或不完整的语句也能生成语法树,这对于实时编辑和自动补全至关重要。
Document Lifecycle
1. Splitting the Source
When opening a new document, a custom statement splitter cuts the file (which may contain invalid or incomplete statements) into individual statements so
libpg_querycan process them independently.Inspired by Pratt Parsing, the splitter uses smart heuristics (e.g., knowing that a
SELECTcannot be followed by anotherSELECTunless it is a sub-statement). If parsing fails, it falls back to splitting at semicolons or double newlines.2. Identifying Statements
Each statement is assigned a unique identifier within the document and tracked via a
Statementstruct:/// Globally unique statement #[derive(Hash)] pub(crate) struct Statement { /// Path of the document pub(crate) path: PgTPath, /// Unique id within the document pub(crate) id: StatementId, }3. Parsing the Statements
The server runs both
tree-sitterandlibpg_queryon the statements: *libpg_queryprovides precise parsing, syntax error detection, and AST structures for diagnostics. *tree-sittergenerates syntax trees even for malformed or incomplete statements, making it invaluable for live editing and autocompletion.
加载架构信息
为了提供上下文分析,服务器会采用类似于 PostgREST 的方式,延迟填充(lazily populates)一个内存中的架构缓存:
pub struct SchemaCache {
pub schemas: Vec<Schema>,
pub tables: Vec<Table>,
pub functions: Vec<Function>,
pub types: Vec<PostgresType>,
pub versions: Vec<Version>,
pub columns: Vec<Column>,
}
Loading Schema Information
To provide contextual analysis, the server lazily populates an in-memory schema cache similar to PostgREST:
pub struct SchemaCache { pub schemas: Vec<Schema>, pub tables: Vec<Table>, pub functions: Vec<Function>, pub types: Vec<PostgresType>, pub versions: Vec<Version>, pub columns: Vec<Column>, }
提供诊断与处理变更
- 诊断(Diagnostics): 服务器将
libpg_query的 AST 传递给类型检查器(该检查器使用 Postgres 的PREPARE语句来捕获数据库级别的类型错误),并结合受 Squawk 启发的 linter 进行代码检查。 - 响应式编辑(Responsive Edits): 由于 SQL 语句是独立的,文本编辑只需要将修改后的语句作废,并调整后续语句的范围——从而避免了对整个文档进行重新解析。
Providing Diagnostics & Processing Changes
- Diagnostics: The server passes the
libpg_queryAST to a type checker (which uses PostgresPREPAREstatements to catch database-level type errors) and a linter inspired by Squawk.- Responsive Edits: Because SQL statements are independent, text edits only require invalidating the modified statement and shifting the ranges of subsequent statements—avoiding full-document re-parsing.
响应式自动补全
自动补全完全依赖于 tree-sitter(用于处理不完整的语句)以及内存中的 SchemaCache(避免了昂贵的数据库查询)。评分算法会根据上下文评估建议:
* 如果用户位于 SELECT 子句内部,表建议的权重会降低。
* 如果存在 FROM 子句,该表的列建议权重会提高。
评分逻辑示例:
fn check_matches_schema(
/// `self` 是当前被考察的建议项。
&mut self,
/// `ctx` 包含有关更改后的 CST 节点以及包含该语句的信息。
ctx: &CompletionContext
) {
let schema_name = match ctx.schema_name.as_ref() {
None => return,
Some(n) => n,
};
let data_schema = self.get_schema_name();
if schema_name == data_schema {
self.score += 25;
} else {
self.score -= 10;
}
}
Responsive Autocompletion
Autocompletion relies exclusively on
tree-sitter(to handle incomplete statements) and the in-memorySchemaCache(avoiding expensive database queries). A scoring algorithm evaluates suggestions based on context: * If the user is inside aSELECTclause, table suggestions are down-ranked. * If aFROMclause is present, column suggestions for that table are up-ranked.Example scoring logic:
fn check_matches_schema( /// `self` is the currently investigated suggestion item. &mut self, /// `ctx` contains the information about the changed CST node and the /// containing statement. ctx: &CompletionContext ) { let schema_name = match ctx.schema_name.as_ref() { None => return, Some(n) => n, }; let data_schema = self.get_schema_name(); if schema_name == data_schema { self.score += 25; } else { self.score -= 10; } }
不仅仅是一个语言服务器
尽管语言服务器是核心重点,但底层的 API 工作空间采用了受 Biome 启发 的客户端-服务器架构。这使得工具链能够通过以下方式运行:
* 通过 CLI(postgrestools check test.sql)
* 在 HTTP API 背后运行
* 最终通过 Wasm 直接在浏览器中运行
Not Just a Language Server
While the Language Server is the primary focus, the underlying workspace API is built with a client-server architecture inspired by Biome. This allows the toolchain to run: * Via the CLI (
postgrestools check test.sql) * Behind an HTTP API * Eventually via Wasm directly in the browser
未来展望
接下来的里程碑包括: * 提高稳定性并修复边缘用例(edge cases)。 * 发布安装指南和更多的编辑器扩展。 * 增加对 PL/pgSQL 的支持、针对 pglite 的 Wasm 构建版本,以及对 SQL 函数体的解析。
热烈欢迎社区的贡献、错误报告和反馈!
What’s Next
Upcoming milestones include: * Improving stability and fixing edge cases. * Publishing installation guides and additional editor extensions. * Adding PL/pgSQL support, a Wasm build for pglite, and parsing SQL function bodies.
Contributions, bug reports, and feedback from the community are warmly welcomed!